制作 K 線的 Data Model,從前面文章 [加權指數K線圖實作.2] 的 response 可以知道,我們的 csv 檔需要 [開]、[高]、[低]、[收] 四個值。考慮到台股所有股票都會用到技術分析,再加上 stockCode 和 stockName,讓這個 Data Model 可以用在其他股票項目上。
import Foundation
struct StockKLine {
let stockCode: String
let stockName: String
let dateString: String
let openString: String
let highestString: String
let lowestString: String
let closeString: String
}
在完成 Data Model 的宣告之後,就回去修改 TwStockKLineManager。之前傳出來的是 String,但現在已經有 StockKLine 的 Data Model 了,那就應該傳出對應的型別,而不是 String了。
TwStockKLineManager parse csv,並傳出 KLine model 的程式碼如下
import Foundation
//加權指數-公開資訊觀測站 https://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.html
// https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907
class TwStockKLineManager {
private var dateUtility: DateUtility {
return DateUtility()
}
private lazy var alamofireAdapter: AlamofireAdapter = {
return AlamofireAdapter()
}()
func requestTwStockKLine(date: Date, completion: @escaping (([StockKLine], Error?) -> Void)) {
let format = "yyyyMMdd"
let string = dateUtility.getString(date: date, format: format)
let urlString = "https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=\(string)"
alamofireAdapter.requestForString(urlString, method: .get) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let string):
let kLineDataSet = self.convertToKLineList(string)
completion(kLineDataSet, nil)
case .failure(let error):
completion([], error)
}
}
}
private func convertToKLineList(_ string: String) -> [StockKLine] {
var kLineDataSet = [StockKLine]()
let trimmedString = trimmedFirstLine(string)
if let csv = try? CSVAdapter(rawString: trimmedString) {
for each in csv.namedRows {
let stockCode = "taiex"
let stockName = "台股加權指數"
let dateString = each["Date"] ?? ""
let openString = each["Opening Index"] ?? ""
let highestString = each["Highest Index"] ?? ""
let lowestString = each["Lowest Index"] ?? ""
let closeString = each["Closing Index"] ?? ""
let kLine = StockKLine(stockCode: stockCode, stockName: stockName, dateString: dateString, openString: openString, highestString: highestString, lowestString: lowestString, closeString: closeString)
kLineDataSet.append(kLine)
}
}
return kLineDataSet
}
private func trimmedFirstLine(_ string: String) -> String {
return CSVAdapter.removeLine(string, at: 1)
}
}
接下來,在 TwStockMarketKLineModel 中,加上讓 VC 呼叫,可取得 K 線資料。在呼叫後,只有 Model 知道要拿取幾個月份的資料,因前述設定,為取得當月和上一個月的 K 線資料,所以 requestTwExKLineInfo() 會呼叫 requestTwExThisMonthKLineInfo() 和 requestTwExLastMonthKLineInfo()。
/// 會取這個月和前一個月台股加權指的 KLine data,單一個月,有可能 k 棒數量太少
func requestTwExKLineInfo() {
requestTwExThisMonthKLineInfo()
requestTwExLastMonthKLineInfo()
}
private func requestTwExThisMonthKLineInfo() {
let date = dateUtility.getStartOfMonth()
manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
self?.update(kLineDataSet)
}
}
private func requestTwExLastMonthKLineInfo() {
let date = dateUtility.getLastMonthStartDate()
manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
self?.update(kLineDataSet)
}
}
private func update(_ dataSet: [StockKLine]) {
let updatedData = Set(self.twExStockDataSet + dataSet)
self.twExStockDataSet = Array(updatedData).sorted { $0.dateString < $1.dateString }
}
在完成後,使用 delegate pattern 通知 VC 有資料更新。
整個 TwStockMarketKLineModelDelegate 的程式碼如下
import Foundation
protocol TwStockMarketKLineModelDelegate: AnyObject {
func didRecieveTaiEx(kLineDataSet: [StockKLine], error: Error?)
}
class TwStockMarketKLineModel {
weak var delegate: TwStockMarketKLineModelDelegate?
private var dateUtility: DateUtility {
return DateUtility()
}
private lazy var manager: TwStockKLineManager = {
return TwStockKLineManager()
}()
var twExStockDataSet = [StockKLine]()
/// 會取這個月和前一個月台股加權指的 KLine data,單一個月,有可能 k 棒數量太少
func requestTwExKLineInfo() {
requestTwExThisMonthKLineInfo()
requestTwExLastMonthKLineInfo()
}
private func requestTwExThisMonthKLineInfo() {
let date = dateUtility.getStartOfMonth()
manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
self?.update(kLineDataSet)
}
}
private func requestTwExLastMonthKLineInfo() {
let date = dateUtility.getLastMonthStartDate()
manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
self?.update(kLineDataSet)
}
}
private func update(_ dataSet: [StockKLine]) {
let updatedData = Set(self.twExStockDataSet + dataSet)
self.twExStockDataSet = Array(updatedData).sorted { $0.dateString < $1.dateString }
}
}
然後在 VC 中設定好成為發動 Model 的 Button action,並成為 Model 的 delegate 即可。
import UIKit
class TwStockMarketKLineViewController: UIViewController {
@IBOutlet weak var debugTextView: UITextView!
private lazy var model: TwStockMarketKLineModel = {
let model = TwStockMarketKLineModel()
model.delegate = self
return model
}()
// MARK: - life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - IBAction
@IBAction func fetchTwStockKLineButtonDidTap(_ sender: Any) {
model.requestTwExKLineInfo()
}
@IBAction func dubugButtonDidTap(_ sender: Any) {
var string = ""
for kLine in model.twExStockDataSet {
string += "\(kLine)\n"
}
debugTextView.text = string
}
}
extension TwStockMarketKLineViewController: TwStockMarketKLineModelDelegate {
func didRecieveTaiEx(kLineDataSet: [StockKLine], error: Error?) {
print(model.twExStockDataSet)
}
}
為了 debug 方便,在這邊加上 textView 和 debug button
接下來,就可以專心的畫圖了,因為你已經取得了需要呈現的資料。
下方是這次 D1 ~ D12 的完成品,可以下載來試
App Store - 台股申購日曆